import {
    GraphNode,
    GraphEdge,
    Graph,
    VisualGraphNode,
    VisualGraph,
    VisualGraphEdge,
} from "../model/graph";

export enum LayoutMode {
    Vertical,
    Horizontal,
}

export class LayoutOption {
    mode: LayoutMode = LayoutMode.Horizontal;
    // maxWidth: number = 1000;
    // maxHeight: number = 1000;
    levelInterval: number = this.mode === LayoutMode.Vertical ? 150 : 300;
    nodeMinInterval: number = this.mode === LayoutMode.Vertical ? 300 : 150;
    selectedRoot: GraphNode | null | undefined;
    useMaxNodeLevel: boolean = false;    // DFS or BFS
    useDirection: boolean = false;
    layoutOptimize: boolean = true;
    edgeLabelEnabled: boolean = false;
    edgePortEnabled: boolean = false;
    staggerOffset: number = 0;
    nodeDisplayType: string = "rect";
}

export function genDAG(graph: Graph, options: LayoutOption): VisualGraph {
    let root = options.selectedRoot;
    if (!root) {
        // find root node: node with no in-degree
        root = graph.nodes.find((node) => node.inDegree() === 0);
        if (!root && !options.useDirection) {
            // or node with minimum in-degree+out-degree
            root = graph.nodes.reduce((prev, curr) => {
                if (
                    prev.inDegree() + prev.outDegree() <
                    curr.inDegree() + curr.outDegree()
                ) {
                    return prev;
                }
                return curr;
            });
        }
        if(!root) {
            throw new Error("No root node found");
        }
    }
    const visualRoot = new VisualGraphNode(root, 0, 0, 0);
    const visualGraphNodes = [ visualRoot ];
    const visualGraphEdges = [] as VisualGraphEdge[];
    // BFS or DFS
    const queue: VisualGraphNode[] = [ visualRoot ];
    const visitedNodes = new Set<string>().add(root.id);
    const visitedEdges = new Set<string>();
    const levelMap = new Map<number, VisualGraphNode[]>();
    while (queue.length > 0) {
        let node = options.useMaxNodeLevel ? queue.pop() as VisualGraphNode : queue.shift() as VisualGraphNode;
        levelMap.set(
            node.level,
            (levelMap.get(node.level) || []).concat(node)
        );
        // console.log(node.name)
        const edgeDict = {
            0: 'target',
            1: 'source'
        }
        for (let i=0; i<2;i++) {
            const edgeList = i === 0 ? node.outgoingEdges : node.incomingEdges;
            for (const edge of edgeList) {
                let sourceOrTarget = i === 0 ? edge.target : edge.source;
                if (!visitedNodes.has(sourceOrTarget.id)) {
                    const visualGraphNode = new VisualGraphNode(
                        sourceOrTarget,
                        0,
                        0,
                        node.level + 1
                    );
                    visualGraphNode.directPredList.push(node);
                    node.directSuccList.push(visualGraphNode);
                    queue.push(visualGraphNode);
                    visitedNodes.add(sourceOrTarget.id);
                    visualGraphNodes.push(visualGraphNode);
                    visualGraphEdges.push(i === 0 ? new VisualGraphEdge(node, visualGraphNode) : new VisualGraphEdge(visualGraphNode, node));
                } else if (!visitedEdges.has(`${edge.sourceId()}->${edge.targetId()}`)) {
                    const visualGraphNode = visualGraphNodes.find((n) => n.id === sourceOrTarget.id) as VisualGraphNode
                    visualGraphEdges.push(i === 0 ? new VisualGraphEdge(node, visualGraphNode) : new VisualGraphEdge(visualGraphNode, node));
                    if(visualGraphNode.level === node.level + 1) {
                        visualGraphNode.directPredList.push(node);
                        node.directSuccList.push(visualGraphNode);
                    }
                }
                visitedEdges.add(`${edge.sourceId()}->${edge.targetId()}`);
            }
            if (options.useDirection) continue;
        }
    }
    // update x, y
    const nodeMinInterval = options.nodeMinInterval;
    const levelInterval = options.levelInterval;
    const nodeInvDir = options.mode === LayoutMode.Horizontal ? 'y' : 'x';
    const nodeLevelDir = options.mode === LayoutMode.Horizontal ? 'x' : 'y';
    const initPos = {x: 0, y: 0};
    for(const [level, nodes] of levelMap) {
        for(let i = 0; i < nodes.length; i++) {
            const node = nodes[i];
            node[nodeLevelDir] = initPos[nodeLevelDir] + level * levelInterval;
            node[nodeInvDir] = initPos[nodeInvDir] + i * nodeMinInterval;
        }
    }
    const visualGraph = new VisualGraph(visualGraphNodes, visualGraphEdges, visualRoot, levelMap);
    if (options.layoutOptimize) {
        layoutDAG(visualGraph, options);
    }
    return visualGraph;
}


export function layoutDAG(visualGraph: VisualGraph, options: LayoutOption): void {
    const levelMap = visualGraph.levelMap;
    const nodeMinInterval = options.nodeMinInterval;
    const nodeInvDir = options.mode === LayoutMode.Horizontal ? 'y' : 'x';
    for (const [level, nodes] of levelMap) {
        if (level === 0) continue;
        for (const node of nodes) {
            // console.log(node)
            let curNode = node as VisualGraphNode | null;
            // let lastNode = curNode as VisualGraphNode;
            while (!!curNode) {
                const succCount = curNode.directSuccList.length;
                if (succCount === 0) {
                    curNode = curNode.directPred();
                    continue;
                }
                arrangeBySuccNode(succCount, curNode, nodeInvDir);
                levelNodeArrange(levelMap, curNode, nodeInvDir, nodeMinInterval);
                if(curNode.id === curNode.directPred()?.id) break;
                // lastNode = curNode;
                curNode = curNode.directPred();
            }
        }
    }
}

function arrangeBySuccNode(succCount: number, curNode: VisualGraphNode, nodeInvDir: 'x' | 'y'): void {
    if (succCount % 2 === 0) {
        curNode[nodeInvDir] =
            (curNode.directSuccList[succCount / 2 - 1][nodeInvDir] 
                + curNode.directSuccList[succCount / 2][nodeInvDir]) / 2;
    } else {
        curNode[nodeInvDir] =
            curNode.directSuccList[Math.floor(succCount / 2)][nodeInvDir];
    }
}

function levelNodeArrange(levelMap:Map<number, VisualGraphNode[]>, curNode: VisualGraphNode, nodeInvDir: 'x' | 'y', nodeMinInterval: number): void {
    const levelList = levelMap.get(curNode.level) || [] as VisualGraphNode[];
    const curNodeIndex = levelList.indexOf(curNode);
    for (let i = curNodeIndex - 1; i >= 0; i--) {
        const peer = levelList[i];
        const lastPeer = levelList[i + 1];
        if (peer[nodeInvDir] + nodeMinInterval > lastPeer[nodeInvDir]) {
            peer[nodeInvDir] = lastPeer[nodeInvDir] - nodeMinInterval;
        } 
        else if (lastPeer[nodeInvDir] - peer[nodeInvDir] > nodeMinInterval) {
            peer[nodeInvDir] = lastPeer[nodeInvDir] - nodeMinInterval;
        }
    }
    for (let i = curNodeIndex + 1;curNodeIndex > -1 && i < levelList.length; i++) {
        const peer = levelList[i];
        const lastPeer = levelList[i - 1];
        if (peer[nodeInvDir] - nodeMinInterval < lastPeer[nodeInvDir]) {
            peer[nodeInvDir] = lastPeer[nodeInvDir] + nodeMinInterval;
        } 
        else if(peer[nodeInvDir] - lastPeer[nodeInvDir] > nodeMinInterval) {
            peer[nodeInvDir] = lastPeer[nodeInvDir] + nodeMinInterval;
        }
    }
}